Skip to content

feat: add development automation tools - #175

Merged
d-ulker merged 14 commits into
mainfrom
feat/add-development-automation
Sep 22, 2025
Merged

feat: add development automation tools#175
d-ulker merged 14 commits into
mainfrom
feat/add-development-automation

Conversation

@d-ulker

@d-ulker d-ulker commented Sep 22, 2025

Copy link
Copy Markdown
Owner

🎯 Purpose

Add essential development automation tools to support fortress-compliant development workflow.

📋 Changes

  • Makefile: Quality commands (format, lint, test, quality-check)
  • scripts/check_pr_scope.py: PR size validation script (≤50 files, ≤1500 lines)
  • .github/workflows/pr-scope-check.yml: Automated PR validation workflow

🏰 Fortress Compliance

Files: 3/5 (well within limit)
Purpose: Single concern (development automation)
Scope: Focused on tooling only
Branch: From main (not monster PR)
Size: Small, reviewable changes

🔄 Extraction Details

🧪 Testing

  • ✅ Pre-commit hooks pass
  • ✅ Fortress guard validation (≤5 files)
  • ✅ No syntax errors
  • ✅ All workflows valid YAML

📊 Impact

  • Developer Experience: Standardized quality commands
  • PR Quality: Automated size validation
  • CI/CD: Prevents monster PRs at GitHub level
  • Workflow: Foundation for fortress-compliant development

This is the first of 10-15 systematic extractions from the monster PR. Each extraction follows strict fortress compliance rules.

🤖 Generated with Claude Code

Summary by Sourcery

Add foundational development automation by providing a Makefile with quality and scope-check commands, a Python script to enforce PR size and naming conventions, and a GitHub Actions workflow to run these validations on pull requests.

New Features:

  • Add Makefile with standardized code quality and scope-check targets
  • Introduce scripts/check_pr_scope.py to enforce PR size, naming, and single-purpose rules
  • Add GitHub Actions workflow for automated PR scope and branch naming validation

Summary by CodeRabbit

  • Chores
    • Added a CI workflow to enforce branch naming, PR size limits, and mixed-concern checks with clear pass/fail messages.
    • Added a local Makefile to run formatting, linting, tests (with coverage), security/type checks, scope checks, and cleanup targets.
    • Added a reusable PR scope checker and centralized configuration for file-type, size, and branch/commit rules.
    • Added optional pre-commit hook installation and commit template support.

Add Makefile, PR scope validation script, and GitHub workflow to support
fortress-compliant development process and prevent monster PRs.

- Makefile: quality commands (format, lint, test)
- check_pr_scope.py: validates PR size limits
- pr-scope-check.yml: GitHub workflow automation

Extracted from monster PR #171 as part of systematic decomposition.
Tracked in issue #174.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 22, 2025 17:57
@sourcery-ai

sourcery-ai Bot commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR adds a standardized development automation toolchain—including a Makefile, a PR scope validation script, and a GitHub Actions workflow—to enforce code quality and guardrails against oversized or mixed-concern pull requests.

File-Level Changes

Change Details Files
Added Makefile for standard quality commands and targets
  • Defined help, format, lint, test, and quality-check commands
  • Added check-scope target to invoke the PR scope validator
  • Included pre-commit-install and clean utilities
Makefile
Introduced a PR scope validation script
  • Implemented git stats collection (files and lines changed)
  • Enforced commit message and branch naming conventions
  • Added mixed-concern detection and a strict mode flag
scripts/check_pr_scope.py
Configured GitHub Actions workflow for automated PR checks
  • Checked out full history and set up Python 3.8
  • Ran the PR scope script with strict failure
  • Validated branch naming and PR size limits within CI
.github/workflows/pr-scope-check.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Sep 22, 2025

Copy link
Copy Markdown

Warning

Rate limit exceeded

@uelkerd has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6fa1e and 1b8ab5b.

📒 Files selected for processing (3)
  • .github/workflows/pr-scope-check.yml (1 hunks)
  • scripts/check_pr_scope.py (1 hunks)
  • scripts/pr_scope_config.py (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a PR scope enforcement system: a GitHub Actions workflow that validates branch naming and PR size, a configurable Python script performing branch/commit/file-type/mixed-concern/size checks, a config module with patterns and thresholds, and a Makefile exposing quality and scope-check targets.

Changes

Cohort / File(s) Summary of Changes
CI workflow: PR scope check
​.github/workflows/pr-scope-check.yml
New GitHub Actions workflow triggered on pull_request. Checks out full history, sets up Python 3.12, upgrades pip, runs prechecks (branch-name and PR size via git diff shortstat) and executes scripts/check_pr_scope.py; fails on violations and uses env guards for missing refs.
PR scope checker: script + config
scripts/check_pr_scope.py, scripts/pr_scope_config.py
New Python CLI that resolves commit ranges, validates branch names and commit messages, extracts git stats (files/lines changed), detects file types, checks mixed-concerns against ACCEPTABLE_COMBINATIONS, enforces size limits (configurable), displays changed files, and returns non-zero on failure. pr_scope_config.py centralizes patterns, thresholds and file-type rules.
Developer tooling
Makefile
New Makefile with targets: help, format, lint, test, quality-check, check-scope, pre-commit-install, clean. Integrates ruff, pylint, mypy, bandit, safety, docformatter, pytest coverage and invokes the PR scope checker.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant GH as GitHub
  participant WF as "PR Scope Check\nWorkflow"
  participant Runner as Runner (checkout + Python)
  participant Script as check_pr_scope.py
  participant Git as git

  Dev->>GH: Open/Update Pull Request
  GH-->>WF: trigger pull_request
  WF->>Runner: checkout (full history) & setup Python 3.12
  WF->>Script: run prechecks (env-based)
  Script->>Git: validate base/head refs, git diff --shortstat, list files
  Git-->>Script: files changed, lines changed, file list
  alt prechecks pass
    WF->>Script: run strict scope checks
    Script->>Git: determine commit range, get commit SHAs, diff per commit
    Script->>Script: branch name, commit-msg, file-type, mixed-concern, size checks
    Script-->>WF: exit 0 (pass) or non-zero (fail) with messages
  else prechecks fail
    Script-->>WF: fail with explicit message
  end
  WF-->>GH: report status
Loading
sequenceDiagram
  autonumber
  actor Dev as Developer
  participant Make as Makefile
  participant Script as check_pr_scope.py

  Dev->>Make: make check-scope
  Make->>Script: python scripts/check_pr_scope.py --strict
  Script-->>Make: return exit code (compliance)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Poem

I hop through branches, sniff the diff,
Nibbling lines to catch a whiff.
Names in order, changes neat,
Files aligned, no mixed-up feat.
A rabbit’s thump — CI sings, “OK!” 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: add development automation tools" accurately and concisely summarizes the primary changes—adding a Makefile, PR scope-checking scripts, and a GitHub Actions workflow to automate developer-quality tasks—and uses the conventional "feat:" prefix so reviewers can immediately see the intent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @uelkerd, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes foundational development automation by introducing a Makefile for standardized code quality checks and a Python script integrated with a GitHub Actions workflow to automatically enforce pull request scope and quality guidelines. This initiative aims to prevent large, unmanageable PRs and promote a "fortress-compliant" development workflow, starting with this initial extraction from a previously larger change.

Highlights

  • Development Automation Tools: A new Makefile has been introduced, providing standardized commands for code formatting (format), linting (lint), testing (test), and a comprehensive quality-check target that integrates multiple tools like ruff, pylint, mypy, bandit, safety, and docformatter.
  • PR Scope Validation Script: A new Python script, scripts/check_pr_scope.py, has been added to enforce pull request size limits (maximum 50 files and 1500 lines changed), validate commit message quality for single-purpose adherence, and check branch naming conventions. It also includes logic to detect and warn about mixed concerns based on file types.
  • Automated PR Validation Workflow: A GitHub Actions workflow (.github/workflows/pr-scope-check.yml) has been implemented to automatically run the PR scope validation script on pull request events, ensuring early detection of non-compliant PRs.
  • Fortress Compliance Strategy: This pull request represents the first of several systematic extractions from a larger 'monster PR' (feat: Implement Complete Automated Code Quality Enforcement System #171), aiming to establish and enforce 'fortress-compliant' development practices through small, focused changes, with tracking under issue 🏰 Monster PR #171 Decomposition Tracking #174.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@deepsource-io

deepsource-io Bot commented Sep 22, 2025

Copy link
Copy Markdown
Contributor

Here's the code health analysis summary for commits 260111d..1b8ab5b. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Test coverage LogoTest coverage⚠️ Artifact not reportedTimed out: Artifact was never reportedView Check ↗
DeepSource Python LogoPython❌ Failure
❗ 4 occurences introduced
View Check ↗
DeepSource Terraform LogoTerraform✅ SuccessView Check ↗
DeepSource Secrets LogoSecrets✅ SuccessView Check ↗
DeepSource Shell LogoShell✅ SuccessView Check ↗
DeepSource Docker LogoDocker✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

sourcery-ai[bot]
sourcery-ai Bot previously requested changes Sep 22, 2025

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

Blocking issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • Consolidate duplicate branch naming and size validation logic between the Python script and the GitHub Actions workflow to avoid maintaining two sources of truth.
  • Defaulting git diff stats to HEAD~1 may not capture all PR changes in multi-commit PRs—consider using the PR base branch (origin/${BASE_BRANCH}) by default for more accurate metrics.
  • The PR scope checker script is becoming quite monolithic; consider refactoring static configuration (e.g., acceptable file-type combinations) into a separate module or config file for easier maintenance.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consolidate duplicate branch naming and size validation logic between the Python script and the GitHub Actions workflow to avoid maintaining two sources of truth.
- Defaulting git diff stats to HEAD~1 may not capture all PR changes in multi-commit PRs—consider using the PR base branch (origin/${BASE_BRANCH}) by default for more accurate metrics.
- The PR scope checker script is becoming quite monolithic; consider refactoring static configuration (e.g., acceptable file-type combinations) into a separate module or config file for easier maintenance.

## Individual Comments

### Comment 1
<location> `scripts/check_pr_scope.py:44-57` </location>
<code_context>
+    stdout, stderr, code = run_command(["git", "diff", "--shortstat", range_spec])
+    lines_changed = 0
+    if code == 0 and stdout:
+        # Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
+        shortstat = stdout.strip()
+        if shortstat:
+            parts = shortstat.split(",")
+            for part in parts:
+                part = part.strip()
+                if "insertions" in part or "deletions" in part:
+                    nums = "".join(c for c in part if c.isdigit())
+                    if nums:
+                        lines_changed += int(nums)
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Shortstat parsing may overcount lines if both insertions and deletions are present.

The logic currently sums all digits in parts containing 'insertions' or 'deletions', which may lead to double-counting. Using regex to extract and sum insertion and deletion counts separately would improve accuracy.

```suggestion
    import re
    # Get lines changed using shortstat
    stdout, stderr, code = run_command(["git", "diff", "--shortstat", range_spec])
    lines_changed = 0
    if code == 0 and stdout:
        # Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
        shortstat = stdout.strip()
        if shortstat:
            insertions = 0
            deletions = 0
            ins_match = re.search(r"(\d+)\s+insertions?\(\+\)", shortstat)
            del_match = re.search(r"(\d+)\s+deletions?\(-\)", shortstat)
            if ins_match:
                insertions = int(ins_match.group(1))
            if del_match:
                deletions = int(del_match.group(1))
            lines_changed = insertions + deletions
```
</issue_to_address>

### Comment 2
<location> `scripts/check_pr_scope.py:252-257` </location>
<code_context>
+    if files:
+        file_types = set()
+
+        for file in files:
+            if file.endswith((".py", ".js", ".ts", ".java", ".cpp", ".c", ".h")):
+                file_types.add("code")
</code_context>

<issue_to_address>
**suggestion (bug_risk):** File type detection logic may misclassify files based on substring matches.

Substring checks like 'test' or 'config' may match unintended files (e.g., 'contest.py'). Use more precise methods, such as directory prefixes or exact extensions, to avoid misclassification.

```suggestion
            elif (
                file.startswith("tests/")
                or file.endswith(("_test.py", "_test.js", "_test.ts", "_test.java", "_test.cpp", "_test.c", "_test.h"))
            ):
                file_types.add("tests")
            elif (
                file.startswith("config/")
                or file.endswith((".yml", ".yaml", ".json", ".toml", ".cfg"))
                or file in ("config.py", "config.js", "config.ts", "config.java", "config.cpp", "config.c", "config.h")
            ):
                file_types.add("config")
```
</issue_to_address>

### Comment 3
<location> `Makefile:24` </location>
<code_context>
+	ruff check --diff src/ tests/ scripts/
+	ruff format --check src/ tests/ scripts/
+	pylint --rcfile=pyproject.toml src/ tests/ scripts/
+	mypy src/ scripts/training/ scripts/database/
+	bandit -r src/ -c pyproject.toml
+	bandit -r tests/ -c pyproject.toml -s B101
</code_context>

<issue_to_address>
**suggestion:** Mypy invocation may fail if directories do not exist.

Add checks to ensure these directories exist before running mypy, or use wildcards to prevent errors if they're missing.

```suggestion
	mypy src/**/*.py scripts/training/**/*.py scripts/database/**/*.py
```
</issue_to_address>

### Comment 4
<location> `scripts/check_pr_scope.py:23` </location>
<code_context>
    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
</code_context>

<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location> `scripts/check_pr_scope.py:216` </location>
<code_context>
    base_for_stats = args.base if args.base else f"{args.branch}~1"

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))

```suggestion
    base_for_stats = args.base or f"{args.branch}~1"
```

<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.

The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.

It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>

### Comment 6
<location> `scripts/check_pr_scope.py:217` </location>
<code_context>
    head_for_stats = args.head if args.head else args.branch

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if-expression with `or` ([`or-if-exp-identity`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/or-if-exp-identity))

```suggestion
    head_for_stats = args.head or args.branch
```

<br/><details><summary>Explanation</summary>Here we find ourselves setting a value if it evaluates to `True`, and otherwise
using a default.

The 'After' case is a bit easier to read and avoids the duplication of
`input_currency`.

It works because the left-hand side is evaluated first. If it evaluates to
true then `currency` will be set to this and the right-hand side will not be
evaluated. If it evaluates to false the right-hand side will be evaluated and
`currency` will be set to `DEFAULT_CURRENCY`.
</details>
</issue_to_address>

### Comment 7
<location> `scripts/check_pr_scope.py:49-57` </location>
<code_context>
def get_git_stats(
    base: str = "HEAD~1",
    head: str = "HEAD",
) -> Tuple[int, int, List[str]]:
    """Get git statistics for a commit range."""
    # Use git range operator base...head for both name-only and stats
    range_spec = f"{base}...{head}"

    # Get number of files changed
    stdout, stderr, code = run_command(["git", "diff", "--name-only", range_spec])
    if code != 0:
        print(f"❌ Git diff error: {stderr}")
        return 0, 0, []

    files = stdout.split("\n") if stdout else []
    num_files = len([f for f in files if f.strip()])

    # Get lines changed using shortstat
    stdout, stderr, code = run_command(["git", "diff", "--shortstat", range_spec])
    lines_changed = 0
    if code == 0 and stdout:
        # Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
        shortstat = stdout.strip()
        if shortstat:
            parts = shortstat.split(",")
            for part in parts:
                part = part.strip()
                if "insertions" in part or "deletions" in part:
                    nums = "".join(c for c in part if c.isdigit())
                    if nums:
                        lines_changed += int(nums)

    return num_files, lines_changed, files

</code_context>

<issue_to_address>
**issue (code-quality):** Use named expression to simplify assignment and conditional [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
</issue_to_address>

### Comment 8
<location> `scripts/check_pr_scope.py:173` </location>
<code_context>
def main():
    """Main function."""
    import argparse

    parser = argparse.ArgumentParser(description="Check PR scope compliance")
    parser.add_argument(
        "--branch",
        default="HEAD",
        help="Branch to check (default: HEAD)",
    )
    parser.add_argument(
        "--base",
        help="Base branch/commit for range comparison (e.g., main, origin/main)",
    )
    parser.add_argument(
        "--head",
        help="Head branch/commit for range comparison (default: current branch)",
    )
    parser.add_argument(
        "--strict",
        action="store_true",
        help="Strict mode - fail on any warning",
    )
    args = parser.parse_args()

    print("🔍 Checking PR Scope Compliance")
    print("=" * 50)

    all_passed = True

    # Check branch name
    print("📋 Checking branch name...")
    if not check_branch_name_quality():
        all_passed = False

    # Check commit message
    print("\n📝 Checking commit message...")
    if not check_commit_message_quality(args.base, args.head):
        all_passed = False

    # Check file and line limits
    print("\n📊 Checking size limits...")
    # Determine base for git stats
    base_for_stats = args.base if args.base else f"{args.branch}~1"
    head_for_stats = args.head if args.head else args.branch
    num_files, lines_changed, files = get_git_stats(base_for_stats, head_for_stats)

    print(f"   Files changed: {num_files}")
    print(f"   Lines changed: {lines_changed}")

    if num_files > 50:
        print(f"❌ Too many files changed! Max 50 allowed, got {num_files}")
        if args.strict:
            all_passed = False

    if lines_changed > 1500:
        print(f"❌ Too many lines changed! Max 1500 allowed, got {lines_changed}")
        if args.strict:
            all_passed = False

    # List changed files
    if files:
        print("\n📁 Files changed:")
        for file in files[:10]:  # Show first 10
            if file.strip():
                print(f"   - {file}")
        if len(files) > 10:
            print(f"   ... and {len(files) - 10} more")

    # Check for mixed concerns (improved logic to reduce false positives)
    print("\n🎯 Checking for mixed concerns...")
    if files:
        file_types = set()

        for file in files:
            if file.endswith((".py", ".js", ".ts", ".java", ".cpp", ".c", ".h")):
                file_types.add("code")
            elif file.endswith((".md", ".rst", ".txt", ".adoc")):
                file_types.add("docs")
            elif "test" in file.lower() or file.startswith("tests/"):
                file_types.add("tests")
            elif "config" in file.lower() or file.endswith(
                (".yml", ".yaml", ".json", ".toml", ".cfg"),
            ):
                file_types.add("config")
            elif "docker" in file.lower() or "Dockerfile" in file:
                file_types.add("docker")
            elif "api" in file.lower() or "endpoint" in file.lower():
                file_types.add("api")
            elif (
                "model" in file.lower() or "ml" in file.lower() or "ai" in file.lower()
            ):
                file_types.add("ml")

        # Allow reasonable combinations that are commonly acceptable
        acceptable_combinations = [
            {"code", "tests"},  # Feature + tests
            {"code", "tests", "config"},  # Feature + tests + config
            {"code", "tests", "docs"},  # Feature + tests + docs
            {"code", "tests", "config", "docs"},  # Feature + tests + config + docs
            {"code", "config"},  # Code changes with config
            {"code", "docs"},  # Code changes with docs
            {"config", "docs"},  # Config changes with docs
            {"tests", "config"},  # Tests with config
        ]

        # Flag as mixed concerns only if we have more than 4 types OR
        # if we have an unusual combination that's not in acceptable list
        is_mixed_concerns = len(file_types) > 4 or (
            len(file_types) > 2 and file_types not in acceptable_combinations
        )

        if is_mixed_concerns:
            print(f"⚠️  Mixed concerns detected: {', '.join(sorted(file_types))}")
            print("   Consider splitting into separate PRs")
            print(
                "   Acceptable combinations include: code+tests, code+tests+config, etc.",
            )
            if args.strict:
                all_passed = False
        elif len(file_types) > 2:
            # Show info about acceptable combinations but don't fail
            print(f"ℹ️  Multiple file types detected: {', '.join(sorted(file_types))}")
            print("   This combination is acceptable for a single PR")

    print("\n" + "=" * 50)
    if all_passed:
        print("✅ PR Scope Check PASSED")
        return 0
    print("❌ PR Scope Check FAILED")
    print("\n💡 Remember the rules:")
    print("   • Max 50 files changed")
    print("   • Max 1500 lines changed")
    print("   • ONE purpose per PR")
    print("   • Single concern (no mixing API + tests + docs)")
    return 1

</code_context>

<issue_to_address>
**issue (code-quality):** Low code quality found in main - 10% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated
Comment thread Makefile Outdated
Comment thread scripts/check_pr_scope.py

def run_command(cmd: List[str]) -> Tuple[str, str, int]:
"""Run command and return (stdout, stderr, returncode)."""
result = subprocess.run(cmd, capture_output=True, text=True, check=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds essential development automation tools to establish a fortress-compliant development workflow that prevents monster PRs through automated validation.

  • Makefile with standardized quality commands for formatting, linting, testing, and scope validation
  • Python script for comprehensive PR scope validation (file/line limits, commit message quality, branch naming)
  • GitHub workflow for automated PR validation on pull request events

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
Makefile Adds quality commands matching pre-commit hooks and PR scope validation
scripts/check_pr_scope.py Comprehensive PR validation script with git analysis and concern detection
.github/workflows/pr-scope-check.yml GitHub Actions workflow for automated PR scope enforcement

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated
Comment thread .github/workflows/pr-scope-check.yml Outdated
Comment thread scripts/check_pr_scope.py Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces valuable development automation tools, including a Makefile for standardized quality commands and a Python script to enforce pull request scope. These are excellent additions for maintaining a healthy and reviewable codebase. The implementations are solid, and my feedback focuses on improving the robustness, maintainability, and correctness of these new tools.

Comment thread scripts/check_pr_scope.py
Comment on lines +46 to +59
lines_changed = 0
if code == 0 and stdout:
# Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
shortstat = stdout.strip()
if shortstat:
parts = shortstat.split(",")
for part in parts:
part = part.strip()
if "insertions" in part or "deletions" in part:
nums = "".join(c for c in part if c.isdigit())
if nums:
lines_changed += int(nums)

return num_files, lines_changed, files

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The function doesn't handle the case where the git diff --shortstat command fails. If it returns a non-zero exit code, the function will silently proceed and return lines_changed = 0, masking a potential error. You should check the exit code and report the error, similar to how you handle the first git diff command.

Suggested change
lines_changed = 0
if code == 0 and stdout:
# Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
shortstat = stdout.strip()
if shortstat:
parts = shortstat.split(",")
for part in parts:
part = part.strip()
if "insertions" in part or "deletions" in part:
nums = "".join(c for c in part if c.isdigit())
if nums:
lines_changed += int(nums)
return num_files, lines_changed, files
lines_changed = 0
if code != 0:
print(f"❌ Git diff --shortstat error: {stderr}")
elif stdout:
# Parse shortstat format like "1 file changed, 10 insertions(+), 2 deletions(-)"
shortstat = stdout.strip()
if shortstat:
parts = shortstat.split(",")
for part in parts:
part = part.strip()
if "insertions" in part or "deletions" in part:
nums = "".join(c for c in part if c.isdigit())
if nums:
lines_changed += int(nums)
return num_files, lines_changed, files

Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py Outdated
- Add proper error handling for git diff --shortstat command failure
- Fix Makefile .PHONY declaration to include all targets
- Remove --fix flag from lint target to make it CI-friendly
- Update mypy paths to include all relevant directories
- Move argparse import to top of file per PEP 8
- All hardcoded constants already moved to config module

Addresses all feedback from gemini-code-assist bot code review.
- Improve test file detection with regex patterns and directory prefixes
- Fix overlapping file categorization to allow multiple types per file
- Update Python version from 3.8 to 3.12 in GitHub workflow
- Magic number 4 already properly defined as MAX_FILE_TYPES_FOR_MIXED_CONCERNS

The new detection logic correctly handles:
- tests/test_model.py -> {tests, ml, code}
- api_model.py -> {api, ml, code}
- test_api.py -> {tests, api, code}

Addresses all feedback from Copilot AI code review.
- Add try/except block to handle ImportError when running from different directories
- Fallback adds script directory to sys.path to ensure module can be found
- Script now works correctly when run from any directory

Fixes ModuleNotFoundError: No module named 'pr_scope_config'
@d-ulker d-ulker self-assigned this Sep 22, 2025
- Add comprehensive security validation to run_command function
- Restrict to git commands only with whitelist of allowed subcommands
- Add detailed documentation explaining security measures
- Prevent command injection by validating command structure
- Only allow read-only git operations: diff, log, rev-list, branch, show

Addresses Sourcery AI security warning about dangerous subprocess use.
All commands are now validated and restricted to safe git operations.
@d-ulker
d-ulker force-pushed the feat/add-development-automation branch from b5ad849 to e4e2276 Compare September 22, 2025 18:19
- Remove redundant 'import os' and 'import sys' in ImportError handler
- These modules are already imported at the top of the file
- Fixes PYL-W0404 linting warning about multiple imports

The script functionality remains unchanged while eliminating code duplication.
- Remove unused 'file_path' parameter from _check_keywords function
- Function only uses 'file_lower' for keyword pattern matching
- Update function call in _check_patterns to match new signature
- Fixes PYL-W0613 linting warning about unused arguments

Functionality remains unchanged - file type detection works correctly.
- Convert 'elif' to separate 'if' statement after return
- Improves code readability and maintainability
- Fixes PYL-R1705 linting warning about unnecessary else/elif after return

The logic remains identical but follows Python best practices.
- Remove unused 'shlex' import from scripts/check_pr_scope.py
- The module was imported but never used in the code
- Fixes PY-W2000 linting warning about unused imports

Script functionality remains unchanged.
- Break long lines to fit within 88-character limit
- Split long function signatures across multiple lines
- Break long string literals and f-strings appropriately
- Reformat long lists and dictionaries for better readability
- Fixes 17 instances of line length violations

All functionality preserved while improving code readability.
- Break long comment into multiple lines to fit within 79-character limit
- Improves code readability and follows Python docstring guidelines
- Fixes FLK-W505 linting warning about docstring line length

Comment now properly formatted across multiple lines.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
.github/workflows/pr-scope-check.yml (3)

33-45: Ensure this step still runs if the previous step fails.
Add if: always() so you always get branch naming feedback even when the Python script fails earlier.

-    - name: Check branch naming (secure)
+    - name: Check branch naming (secure)
+      if: always()
       env:
         BRANCH_NAME: ${{ github.head_ref }}

46-50: Same: always run size check for complete diagnostics.

-    - name: Check PR size limits (secure)
+    - name: Check PR size limits (secure)
+      if: always()
       env:
         BASE_BRANCH: ${{ github.base_ref }}
-        HEAD_REF: ${{ github.head_ref }}

51-56: HEAD_REF is unused; input validation can be simplified.
Also, when diffing a PR, prefer triple‑dot against HEAD.

-        # Validate input parameters
-        if [[ -z "$BASE_BRANCH" || -z "$HEAD_REF" ]]; then
+        # Validate input parameters
+        if [[ -z "$BASE_BRANCH" ]]; then
           echo "❌ Missing required branch information"
           exit 1
         fi
Makefile (2)

2-2: Add a conventional default target all to satisfy checkmake and provide a single entry point.

-.PHONY: help format lint test quality-check clean check-scope pre-commit-install
+.PHONY: all help format lint test quality-check clean check-scope pre-commit-install
+
+all: quality-check  ## Default: run full quality suite

33-36: Guard commit template usage.

Makefile sets git commit.template to .gitmessage.txt but that file is missing — only set the config if the file exists (or fail fast). File: Makefile (lines 33–36).

Suggested Makefile change:

pre-commit-install: ## Install pre-commit hooks
	pre-commit install
	@if [ -f .gitmessage.txt ]; then \
		git config commit.template .gitmessage.txt; \
	else \
		printf '%s\n' "ℹ️ .gitmessage.txt not found — skipping commit.template"; \
	fi
scripts/pr_scope_config.py (1)

31-33: API detection via bare “api/endpoint” keywords is noisy.
Consider directory or spec-file driven rules to reduce false positives.

-    "api": {
-        "keywords": ["api", "endpoint"],
-    },
+    "api": {
+        "directories": ["api/", "apis/", "server/", "routes/"],
+        "patterns": [r"(^|/)(openapi|swagger)\.(ya?ml|json)$", r"(^|/)(routes?|controllers?)(/|$)"],
+    },
scripts/check_pr_scope.py (2)

72-76: Allow safe git subcommands used for robust branch detection.

-    allowed_git_commands = {
-        "diff", "log", "rev-list", "branch", "show"
-    }
+    allowed_git_commands = {
+        "diff", "log", "rev-list", "branch", "show", "symbolic-ref"
+    }

307-314: Treat “ml” as a subtype of code to avoid false “mixed concerns.”
This PR is tooling-only but repo paths can include “ml”; don’t count it toward mixing.

-    # Flag as mixed concerns only if we have more than MAX_FILE_TYPES_FOR_MIXED_CONCERNS types OR
-    # if we have an unusual combination that's not in acceptable list
-    is_mixed_concerns = len(file_types) > MAX_FILE_TYPES_FOR_MIXED_CONCERNS or (
-        len(file_types) > MAX_FILE_TYPES_FOR_WARNING and file_types not in ACCEPTABLE_COMBINATIONS
-    )
+    # Treat domain subtypes (e.g., 'ml') as part of 'code' for mixing evaluation
+    effective_types = set(t for t in file_types if t not in {"ml"})
+    is_mixed_concerns = len(effective_types) > MAX_FILE_TYPES_FOR_MIXED_CONCERNS or (
+        len(effective_types) > MAX_FILE_TYPES_FOR_WARNING and effective_types not in ACCEPTABLE_COMBINATIONS
+    )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 260111d and 84f7f19.

📒 Files selected for processing (4)
  • .github/workflows/pr-scope-check.yml (1 hunks)
  • Makefile (1 hunks)
  • scripts/check_pr_scope.py (1 hunks)
  • scripts/pr_scope_config.py (1 hunks)
🧰 Additional context used
🪛 checkmake (0.2.2)
Makefile

[warning] 2-2: Missing required phony target "all"

(minphony)

🪛 GitHub Actions: PR Scope Check
scripts/check_pr_scope.py

[error] 1-1: PR Scope Check FAILED. Mixed concerns detected: code, config, ml. Please split changes to adhere to PR guidelines (max 50 files, max 1500 lines, one purpose per PR).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (6)
.github/workflows/pr-scope-check.yml (3)

15-18: Checkout depth is correct for diffing — good call.
Full history avoids misleading stats from shallow clones.


19-23: Python 3.12 selection LGTM.
Matches prior suggestion to move off 3.8.


3-6: Minimal permissions — good.
Appropriate least‑privilege for this workflow.

Makefile (1)

19-29: Quality suite is well-curated.
Good separation of “format/lint/test” vs “quality-check” parity with CI.

scripts/pr_scope_config.py (1)

58-59: Branch pattern matches workflow regex — good consistency.

scripts/check_pr_scope.py (1)

444-455: Result messaging is clear — nice.
Concise summary plus rules recap.

Comment thread .github/workflows/pr-scope-check.yml Outdated
Comment thread scripts/check_pr_scope.py
Comment thread scripts/check_pr_scope.py Outdated
Comment thread scripts/check_pr_scope.py
Comment thread scripts/pr_scope_config.py
- Fix ML keyword matching to use word boundaries (\bml\b) to avoid false positives like 'html'
- Replace 'machine learning' with 'machine_learning' for better file matching
- Fix git stats range to use full base...HEAD range instead of HEAD~1..HEAD for multi-commit PRs
- Update base_for_stats to use args.branch instead of f'{args.branch}~1'
- Update head_for_stats to use 'HEAD' instead of args.branch

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
scripts/check_pr_scope.py (4)

83-85: Harden subprocess calls with a timeout.

Prevents hangs on large repos or broken Git states; returns a clear error.

Apply:

-    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
-    return result.stdout.strip(), result.stderr.strip(), result.returncode
+    try:
+        result = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=60)
+        return result.stdout.strip(), result.stderr.strip(), result.returncode
+    except subprocess.TimeoutExpired:
+        return "", f"Command timed out: {' '.join(cmd)}", 124

10-12: Update usage to reflect supported flags.

Doc shows only --branch/--strict but args also support --base/--head.

Apply:

-Usage:
-    python scripts/check_pr_scope.py [--branch <branch>] [--strict]
+Usage:
+    python scripts/check_pr_scope.py [--branch <branch>] [--base <base>] [--head <head>] [--strict]

438-444: Show actual acceptable combinations from config.

Reduces confusion when mixed concerns fire in CI.

Apply:

-        print(
-            "   Acceptable combinations include: code+tests, code+tests+config, etc.",
-        )
+        combos_str = ", ".join("+".join(sorted(c)) for c in ACCEPTABLE_COMBINATIONS)
+        print(f"   Acceptable combinations include: {combos_str}")

368-379: Optional: add per-file type debug to diagnose false positives.

When strict mode fails, printing file→types mapping helps explain “ml” detections.

Apply (adjacent helper print):

 def display_changed_files(files: List[str]) -> None:
@@
     for file in files[:MAX_FILES_TO_DISPLAY]:  # Show first MAX_FILES_TO_DISPLAY
         if file.strip():
             print(f"   - {file}")
     if len(files) > MAX_FILES_TO_DISPLAY:
         print(f"   ... and {len(files) - MAX_FILES_TO_DISPLAY} more")
+
+def _debug_file_type_mapping(files: List[str]) -> None:
+    print("\n🔎 File type mapping (debug):")
+    for f in files[:MAX_FILES_TO_DISPLAY]:
+        if not f.strip():
+            continue
+        types = ", ".join(sorted(detect_file_types(f))) or "unknown"
+        print(f"   - {f}: {types}")

And call it right after mixed concerns detection triggers:

-    if is_mixed_concerns:
+    if is_mixed_concerns:
+        _debug_file_type_mapping(files)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84f7f19 and 4a6fa1e.

📒 Files selected for processing (2)
  • scripts/check_pr_scope.py (1 hunks)
  • scripts/pr_scope_config.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/pr_scope_config.py
🧰 Additional context used
🪛 GitHub Actions: PR Scope Check
scripts/check_pr_scope.py

[error] 1-1: PR Scope Check FAILED: Mixed concerns detected (code, config, ml). 4 files changed, 716 lines. Command 'python scripts/check_pr_scope.py --strict' exited with code 1.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (3)
scripts/check_pr_scope.py (3)

126-140: Fork-safe commit range: use HEAD for PR tip; handle partial args.

Using origin/{head_ref} breaks on forked PRs and ignoring partial args is surprising. Return base..HEAD when only base is provided; head^..head when only head is provided; and prefer GITHUB_BASE_REF..HEAD for CI.

Apply:

 def _determine_commit_range(base: Optional[str], head: Optional[str]) -> str:
     """Determine the commit range to check."""
-    if base and head:
-        return f"{base}..{head}"
-
-    # Try to determine range from environment or git context
-    # Check for common CI environment variables
-    if os.environ.get("GITHUB_BASE_REF") and os.environ.get("GITHUB_HEAD_REF"):
-        base_ref = os.environ["GITHUB_BASE_REF"]
-        head_ref = os.environ["GITHUB_HEAD_REF"]
-        return f"origin/{base_ref}..origin/{head_ref}"
-
-    # Fallback to HEAD^..HEAD for single commit
-    return "HEAD^..HEAD"
+    if base and head:
+        return f"{base}..{head}"
+    if base and not head:
+        return f"{base}..HEAD"
+    if head and not base:
+        return f"{head}^..{head}"
+    base_env = os.environ.get("GITHUB_BASE_REF")
+    if base_env:
+        return f"origin/{base_env}..HEAD"
+    return "HEAD^..HEAD"

419-422: Size stats undercount multi-commit PRs; compare base…HEAD in CI, HEAD~1…HEAD locally.

Current default HEAD~1..HEAD misses multiple commits when CI base is known. Use GITHUB_BASE_REF when present; otherwise keep local fallback.

Apply:

-    base_for_stats = args.base or f"{args.branch}~1"
-    head_for_stats = args.head or args.branch
+    base_env = os.environ.get("GITHUB_BASE_REF")
+    base_for_stats = args.base or (f"origin/{base_env}" if base_env else f"{args.branch}~1")
+    head_for_stats = args.head or "HEAD"

211-230: Branch detection fails on detached HEAD; add CI/env and symbolic-ref fallbacks.

Avoid hard-failing on detached HEAD. Prefer GITHUB_HEAD_REF, then symbolic-ref; skip check if still unresolved.

Apply:

 def check_branch_name_quality() -> bool:
     """Check if branch name follows naming conventions."""
-    stdout, stderr, code = run_command(["git", "branch", "--show-current"])
-    if code != 0:
-        print(f"❌ Git branch error: {stderr}")
-        return False
-
-    branch_name = stdout.strip()
-    if not branch_name:
-        print("❌ Could not determine current branch")
-        return False
+    stdout, stderr, code = run_command(["git", "branch", "--show-current"])
+    branch_name = os.environ.get("GITHUB_HEAD_REF") or stdout.strip()
+    if not branch_name:
+        sym_out, sym_err, sym_code = run_command(["git", "symbolic-ref", "-q", "--short", "HEAD"])
+        if sym_code == 0:
+            branch_name = sym_out.strip()
+    if not branch_name:
+        print("ℹ️  Detached HEAD: skipping branch naming check (use workflow branch check).")
+        return True

Note: This requires allowing the 'symbolic-ref' subcommand (see next comment).

Comment thread scripts/check_pr_scope.py
- Update GitHub workflow to use triple-dot merge-base diff (origin/$BASE_BRANCH...HEAD) for accurate PR change measurement
- Fix commit range to use local HEAD instead of origin/{head_ref} for forked PRs
- Improve branch detection for detached HEAD in CI using GITHUB_HEAD_REF environment variable
- Add git symbolic-ref as fallback for branch detection in CI environments
- Add symbolic-ref to allowed git subcommands for security validation
- Add comment explaining that subprocess.run is safe due to command validation
- Clarify that no user input reaches subprocess, preventing command injection
- Address Sourcery AI security warning (false positive)
@d-ulker
d-ulker merged commit 72ccb3e into main Sep 22, 2025
9 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants